Add closed shadow DOM capture via CDP#33
Conversation
Use CDP to discover closed shadow roots before DOM serialization. Closed shadow roots are inaccessible from JS (element.shadowRoot === null), but CDP's DOM domain can pierce them. We resolve each closed shadow root to a JS object and store it in a WeakMap that PercyDOM.serialize() reads. - Add exposeClosedShadowRoots() using CDPSession - Add walkNodes() helper to traverse CDP DOM tree - Skip iframe contentDocument nodes (cross-frame not yet supported) - Non-fatal: catches exceptions for non-Chromium browsers and CDP errors - Called after PercyDOM injection, before DOM serialization Ported from percy/percy-playwright#609 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- page.frames() returns the entire frame tree, including same-origin grandchildren of cross-origin frames. Switching to mainFrame().childFrames() scopes detection to top-level iframes; nested capture is handled inside processFrame via recursion when supported. - Pair every successful DOM.enable with DOM.disable in finally so the CDP session doesn't keep emitting DOM events after capture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit 4afca05 switched from page.frames() to mainFrame().childFrames() on the assumption that processFrame recurses. It doesn't — processFrame only serializes and returns. As a result, topologies like main(A) -> same-origin(A) -> cross-origin(B) silently dropped B. Option A (chosen, minimal): walk the full page.frames() tree and skip frames whose ancestor is already a known cross-origin frame. This preserves correctness for all observed topologies while keeping each top-level CORS frame processed exactly once (descendants are stitched in by the in-frame PercyDOM.serialize call). Picked over Option B (recursive processFrame) because the diff is smaller and the existing test suite's mocking around mockPage.frames() keeps working as-is. Also leaves a TODO in processFrame for closed-shadow exposure inside CORS iframes — that requires a CDP session attached to the OOPIF target, which is materially more than ~30 LOC and is tracked separately. Top-page closed-shadow capture is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A single detached backendNodeId (host or shadow) caused DOM.resolveNode to throw, which previously aborted the whole loop and lost every remaining closed shadow root on the page. Wrap each iteration in its own try/catch so one bad pair only skips itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the three regressions guarded by the BLOCKER fix and the per-host try/catch: - crossOriginGrandchildOfSameOriginFrameIsStillCaptured main(A) -> same-origin(A) -> cross-origin(B): B is emitted. - descendantsOfACorsFrameAreNotRecapturedIndependently main(A) -> cors(B) -> inner(B) -> nested-cors(C): only B is emitted; C is handled inside B's PercyDOM.serialize. - domEnableIsPairedWithDomDisableOnSuccess / OnException DOM.disable always runs after a successful DOM.enable, even when DOM.getDocument throws. The CDP session is always detached. - domDisableNotCalledWhenDomEnableFails If DOM.enable itself throws, DOM.disable is not called (correct protocol shape) but detach still runs. - oneBadBackendNodeIdDoesNotAbortRemainingShadowHosts One DOM.resolveNode failure skips only its pair; the remaining closed shadow hosts are still exposed via Runtime.callFunctionOn. Tests are pure-mock (Mockito) — no Playwright browser required, so they run on the full Java 8/11/17/21 CI matrix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # package.json
Percy.java (PER-7292) imports com.google.gson.* directly for parsing the CDP DOM tree when exposing closed shadow roots, but gson was only present transitively via the Playwright artifact. A direct compile-time source dependency must be declared explicitly so a future Playwright upgrade that drops or relocates gson cannot silently break compilation. Pinned to 2.10.1 to match the version Playwright 1.44.0 currently resolves, so no version skew is introduced. Build and offline test suites unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🤖 stack:pr-review — Automated review (BrowserStack AI harness)Summary: Adds (1) closed shadow DOM capture via CDP (
Findings (fixed): Verification: JDK 17 / Maven 3.9.15. Commits pushed: Overall: ✅ Pass — Approve. No human-decision blockers; pre-existing live-CLI
|
…e capture The Java SDK captured cross-origin iframes but ignored the two opt-out controls the JS SDK already supports, so an iframe a user marked to skip was still captured. Port from percy-playwright/index.js: - resolveIgnoreSelectors: per-snapshot ignoreIframeSelectors, then global cliConfig.snapshot.ignoreIframeSelectors; normalized to a String list. - Per-frame lookup in the parent frame's DOM for data-percy-ignore and ignoreIframeSelectors matches; skip (debug log) before the switch/serialize. Invalid selectors swallowed; empty/missing -> no-op. Adds PercyIframeIgnoreTest (5 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude Code PR ReviewPR: #33 • Head: 824737e • Reviewers: stack:code-reviewer SummaryAdds (1) closed-shadow-DOM capture via a page-level CDP session ( Review Table
FindingsNo blocking issues found. Non-blocking observations (informational):
Note: the PR's Percy "visual change needs review" status is a manual dashboard approval, separate from this code review. Verdict: PASS |
Summary
exposeClosedShadowRoots()method using CDP to discover and expose closed shadow roots before DOM serializationPorted from percy/percy-playwright#609
Test plan
🤖 Generated with Claude Code